Skip navigation and go to content

Form Validation with ARIA Live Regions

On this page

In this lesson, we’ll work with the form on the “Submit Your Spot” page of CampSpots. It can be navigated to from the “Resources” section of the MegaNav.

The “Submit Your Spot” form in the CampSpots app

The form includes inputs, a custom checkbox, and a text area.

As it stands right now, if we submitted the empty form there’s nothing in place that would prevent the page from refreshing. There’s also no error handling or form validation yet.

In this lesson we’ll work on adding a live region as part of an implementation for form validation.

While HTML does offer some native validation, it isn’t super reliable on its own. Rolling our own custom validation is still what I recommend. This also allows us to take a hybrid approach and use a combination of announcements and focus management to alert the user about an invalid form state.

The code for the page can be found in components/page-submit-listing.js. There’s not much more than the markup in that file for the time being, but at least the form inputs are labeled!

The goal for the end of this section is to have the screen reader announce that there are invalid fields without moving the user’s focus.

Video: Lesson Introduction
Loaded: 4%
Current Time 0:00
/
Duration Time 1:59
Video Transcript

So let's add a live region and we'll see what effect it has in voiceover and NVDA.

So the page that we are going to work on is under resources, submit your spot. It's a new page we haven't really fiddled with yet. So it has a series of form inputs checkbox kind of custom checkbox and a text area. And so to, if we were going to submit this form right now, if these fields are empty, there's nothing preventing the page from refreshing and it doesn't tell us any error handling.

And so one use case for live regions is to use it in form validation. So it could be something asynchronous where maybe I'm typing in a field. And I hit the enter key to submit the form, or, you know, it might be updating like maybe there's a format that I need to match or something. So live regions, one use case for them is to apply them in form validation.

So let's, let's play with this. I'm going to bring, I'm going to come over to visual studio code And our page for this is in components. It's page submit listing. And right now it has some markup in it and that's about it. So let's add some functionality. We've got a form down here.

Fortunately, our form inputs are labeled, so we're off to a good start, but we need some form validation to make sure that if someone submits this with screen reader, that they are notified, which fields are invalid and we'll have some messaging that we can announce asynchronously without having to move their focus.

We can move focus as part of form validation. So it's, it's nice to be able to be crafty and selective about where you move focus to. And you might, while you're typing, like I was saying, you might want to make an announcement without moving focus. So that's really that specificity of that interaction is what we're going for with live regions.

Adding a Live Region to the Form

At line 24 inside of the <form> tag but outside of the layout divs, add a paragraph tag.

We will add the role attribute with a value of "alert" and set the aria-relevant attribute to "all". We’ll also add a CSS className of "error" so we can style this as an error message.

💡Tip

The role of alert tells the screen reader to interrupt any current announcements that might be happening. The aria-relevant property of all will update when anything is added or removed from the live region.

Here’s what the code looks like so far:

<form>
	<p className="error" role="alert" aria-relevant="all"></p>
	...

We’ve just created our first live region, ready to receive messages!

The p tag will be present when the page loads with a role and aria-relevant.

We’ll set it up so that it conditionally displays information. There are a couple things that need added to the form before we can do this.

Update the form to have an action

Our form currently doesn’t have an action associated with it.

We’ll add one and let it be an empty string since client-side JavaScript will be taking care of the form validation and submission to start.

We also add an onSubmit attribute that will pass an event object into a function called submitHandler that we’ll write near the top of the file outside of the return statement.

Here’s what our form tag looks like now:

<form
  action=""
  aria-describedby="key"
  onSubmit={event => submitHandler(event)}
>

Track variables in Component State

Before we write the submitHandler we need to set up some state variables for the component to track.

We need to know if the form has been submitted as well as if the form is dirty (fields have been edited but not yet submitted). We also need to keep track of whether there should be an error announcement or not.

At the top of the file where React is imported, we’ll also bring in the useState hook (and useRef for later).

import React, {useState, useRef} from "react"

Following React’s useState pattern, we’ll add our state variables and setter functions with initial boolean values of false:

// Above the `return()` in the component
let [isFormSubmitted, setIsFormSubmitted] = useState(false)
let [isFormDirty, setIsFormDirty] = useState(false)
let [errorAnnouncement, setErrorAnnouncement] = useState(false)

Video: Preparing to add a Live Region
Loaded: 1%
Current Time 0:00
/
Duration Time 6:54
Video Transcript

So inside of our form, let's add a live region. So inside of the form tag, but outside of our series of divs that kind of hold our layout, I'm going to drop down and add a paragraph tag, and we're going to use react for this. So it's most basic to add a live region to this paragraph tag. I am going to say

role of alert. So we're going to make this interrupt. We're going to make it a heavy hammer. It's like, no, really announce this interrupt things. So I'm going to say roll of alert. I'm also going to give this a class name of error so we can style this to be looking like an error message.

I'm also going to say aria relevant, and I'm going to say We'll say all, just make sure it reads everything.

Unfortunately the default HTML validation is not super reliable. And so rolling our own custom validation is still the best way. I hate that. I wish it wasn't the case. And even I played with using, like, there is a pattern down here for email, but not super reliable. So, yeah, I would say if you can use error messaging it also, this gives us an opportunity to have some overall error messaging for all of the fields.

Like if they all need to be filled in we can do a combination of focus management and making these screen reader announcements. But we don't have to always move focus to this text to get it to announce in a screen reader. So we're going to be kind of choosy about what tools we apply, where and when I wish the default error validation worked more reliably, but it does not.

So we've got a live region here. This is present on page load, so it's got a roll of alert. It's got the ARIA relevant of all for just read everything and we need to put some content in it. So I'm gonna make an expression and I'll say error announcement. if there is an error announcement, react, will go and put it here.

I'm going to qualify this a little bit so that it will only show this text under certain circumstances. So, we're going to make, we need to make the concept of our form being dirty. So like when you touch it or you start typing, it, it's sort of in a mixed state where it has content in it. And if we want to keep track of that, so that as the user starts interacting with the form, we can kind of update things and send messages to the screen reader based on the state of the form.

So we're going to have this concept of form dirtiness. I'm going to close our sidebar, give us a little more room. So let's go introduce that before I go and change our, our live region. So we know kind of what we're working with. So we need to add some stuff.

We also need a submit handler. How about we start there?

So we don't have any form action right now. I'm going to add one and just let it be empty, but we're going to add a submit handler. So this form action, we may or may not need that attribute because we're going to let JavaScript do our, it's going to capture the form, submit, prevent it from posting to a server until all these conditions are met and then maybe it would submit server side.

Maybe it would do an asynchronous post to a server. We're going to kind of, imagine how that part can be implemented. We're going to do all the client side validation for this step, but just know that there could be a server side component later, or there should be.

So we need an on submit event. So inside of this on submit, we are going to bind a function and I'm going to say submit handler and.

That will reference a function that we need to write up here. So in our kind of outside of our return statement inside of our functional component for the submit listing page, I'm going to define const submit handler and I will pass in that event object so I'm going to make sure that we pass that in event. We're going to need that so that we can prevent the browser from refreshing. So inside of our submit handler. Let's the first thing that we want to do. Cause we're letting JavaScript do this. When we hit the enter key or we submit this form we don't want the whole page to refresh.

So let's do event dot prevent default. So far. We've seen that with our arrow keys, we were preventing the default behavior of the up and down scrolling of the browser in a very specific use case for this. We're going to prevent the default behavior of our form submitting and refreshing the page. Cause we're not posting to a server side action right away.

We don't want that behavior because we want to capture, we want to do interactive things in the client. So that's what this will do. We're also, we need, we need some state variables here, so let's add some of these things. We need a little bit of setup. So outside of our submit handler, similar to our date picker, let's do some instantiation of various variables and state, state things starting with where we need some state for whether our form is submitted.

So let is form submitted and we'll make a call or a function to set that as well. Set is form submitted. Is it submitted yes or no? So that will use state. The use state hook, which I adore. It's so awesome. So then we need this concept of dirtiness, like as the form hasn't been submitted. So that will be a way for us to kind of check whether we can clean the slate or not.

Or is it in this kind of middle state where we've typed, it's kind of half finished. We need this concept of dirtiness so that we can kind of, update error messages and things accordingly. So is form dirty set is form dirty, and that will be used state of false. So kind of two independent state variables that we're going to set and check.

We also need our error announcement that we have referenced already. So const we'll let this be captured in state as well, error announcement and a way to set that error announcement. And we'll use that in our live region. I'm going to set its default value to an empty string. So for live regions, we're going to use react state for this, but you could append text you know, using Dom methods, you could set text content, you could append a child with another span or another paragraph inside of a div.

You know, it's really just about getting texts to render inside of that live region. We happen to be using react state so that we're kind of keeping track of what this error announcement is and kind of making it appear or not depending on the state of the form, but just know that you can append text to a live region, a variety of ways.

Adding Default State for the Form

We’ll also track formState for the component and initialize the value to an object with default values for all of our form fields:

💡Tip

Up until now, we’ve tracked boolean values in state but React supports a variety of data structures!

const [formState, setFormState] = useState({
  'sitename': '',
  'location': '',
  'fee': 0,
  'legalToCamp': true,
  'submittername': '',
  'email': '',
  'notes': ''
})

Writing the submitHandler function

Our submitHandler function will take in the event object.

The first thing we need to do inside the function is call event.preventDefault() since we don’t want the form to submit and refresh the page.

Then we’ll write some logic that will respond and manage any changes to the form state variables we tracked before.

When the form is submitted, we’ll call setIsFormSubmitted and pass in a true value.

setIsFormSubmitted(true)

We’ll also call setErrorAnnouncement and pass in a message about the required fields that will render inside our live region later on.

Here’s what it looks like so far:

const submitHandler = (event) => {
	event.preventDefault()
	setFormIsSubmitted(true)
	
	setErrorAnnouncement('Required fields cannot be empty.')
}

Video: Adding State & Writing the Submit Handler
Loaded: 3%
Current Time 0:00
/
Duration Time 3:12
Video Transcript

So we've got our error announcement text that we're going to set that to be whatever we want our error message to be. And that is what will get piped to the screen reader. We also need to keep track of some states. So we haven't really done anything yet for individual inputs, but we'll just get some default state for our form.

It's mostly going to be empty, empty strings and things, but I'll say form state set form state. That is going to use state and I'm going to pass an object. We haven't seen that before. So far, we've past booleans for true or false. We've got a string that we're going to use for the error announcement. We had an empty array for, oh, I guess that was used refs.

But these hooks in react, you can work with a variety of different data structures and different objects. So with this one, we're going to pass an object with a bunch of default things for our forms. So we'll have one for site name, that'll be an empty string location. We'll just do a little bit of setup here for our form.

The fee's going to be zero to $0 to start. There's a checkbox for whether it's legal to camp. So if someone doesn't like, oh, I found the school camp site and you're like, well, is it yours? Can people actually camp there? Or are you stealth camping? And we shouldn't put it on our website. Submitter name.

So who are you? That's been submitting this spot to camp at what's their email so we can contact them and then any notes. So some little bit initialization for our form.

So down in our submit handler, we need to do some stuff to respond. If we have any changes in state, we're going to kind of reset variables, starting with our set is form submitted.

So when we hit that submit button, that's submitting the form. So we're going to set that state variable to true. So this, this set is form submitted that will change. This is form submitted to true or false kind of depending on what we pass in. I love that. Cause we can call this function to set it and then we can read this first value in these arrays for the state variables.

So when we submit that form we'll set that. So we also need to do some stuff about our individual form elements. So our form elements, let's go ahead and do that stuff here. So we've got our first state variable. We are also going to keep track of some elements. Let's do a little test. So we've got a whole bunch of code to write.

But I want to kind of show you some of the, the live region functionality before that, that moment. So let's, let's load this up in the browser and sort of see, well, what is it failing on first of all? So. We've got, oh, use state, we need, we need to add some stuff up here. So in react import, we'll say use state let's see.

Do we need anything else? We'll add, use ref just in case, just in case we need some refs I think we will. So that's we were referencing something, we had an imported, so the browser blew up. Okay. So now our pages is good to go.

Checking Progress in the Browser

With the live region element added to the form, we're at a good point to check our progress in the browser.

Since the <p> tag is empty, we'll use DevTools to add text content of "Error!" inside to preview what a message looks like.

Manually entering an Error message in DevToolsLoading

Adding a Global Error Announcement in the Live Region

Inside of the submitHandler function, we have already called setErrorAnnouncement and passed in a global error message about required fields.

We still need to wire it up in our live region.

Inside the <p> tag we added earlier, we’ll use React and JSX to render the dynamic errorAnnouncement:

<p className="error" role="alert" aria-relevant="all">
{errorAnnouncement}
</p>

When setErrorAnnouncement has been called with a string message, it will automatically print inside of our live region and kick off an announcement in a screen reader.

Back in Safari, open up VoiceOver with Fn + CMD + F5.

While VoiceOver is reading the page, submit the form and it will immediately switch to reading our error message while retaining focus on the element. We’ve successfully added our first dynamic live region!

Since we know that the global error functionality is working, let's move on to adding individual error messages for each of the inputs.

Video: Checking Progress in the Browser
Loaded: 2%
Current Time 0:00
/
Duration Time 3:30
Video Transcript

So we've got a live region in here and I want to show it to you. Where does it live?

It's inside of the form. So we've got this error div, right? It doesn't have any content in it right now. And if I add content to it, so I mentioned there's any, a number of ways that you can append content to a live region, including I could just go in here and add some so I could. I could come right in here and say error and hit command enter on my Mac.

And now this field has shown up here. And so when the screen reader is running, it will make that announcement and we could play with it. I think what could get a little bit confusing with it in this state is that the screen reader, when we open, it would also be reading the dev tools, but I wanted to show you when we append text to this, this is what will get read aloud.

So we're going to go and add an error message kind of under specific conditions, but this is where it will go.

So let's go and add just something to test.

So I could say set error announcement occurred. Fields cannot be empty. So that will add that error text when we submit the form. So we've prevented default. So it won't refresh the browser.

Let's test that real quick. So you can get some satisfaction of seeing what is it that we're even talking about with live regions. So I'm going to do function command F five. I can do it in Chrome. Chrome isn't used quite as much as voiceover, but we'll test this particular piece of functionality in Chrome for the time being do function, command f5 voiceover on Chrome camp spots, Google Chrome edit text article less than less than one role equals presentation negates the implicit list and list item role semantics by does not affect the contents greater than less than did role equals M R.

Fun fact, whenever you have the ARIA specification open, it will read it all out in VoiceOver for some reason. I'm not really sure why it's, there's something about the way that that page is coded, that it will take over your screen reader. So I had to close it. I do see our error messages popping up when I hit submit.

So I'm gonna fire voiceover back up and test it. Okay. Function command of five voiceover on Chrome camp spots, Google Chrome window heading level one, camp spots heading level one required fields can not be empty. You are currently on a button. I love that little error message. So it interrupts anything else.

So when we've got an error message that shows up bloop, it will pop up. And share that message. And so we have some of our error validation happening. So that popped up that message without moving my keyboard focus. So say I was in here typing and I, I hit enter on accident. So if I hit refresh, for example, to show you this use case say I'm focused in here, I go, bloop required fields can not be empty.

It has kept my focus in the field I'm on, which is nice. Cause it's, it keeps my me from having to go, oh, now I have to tab all the way back through this form. So independently I can kind of do these two things like making an announcement in the screen reader show an error message. Keep my focus where it was.

So that's kind of our global message.

We also could do individual level inputs, which I was starting to do. So let's, let's finish that stuff out.

💡Tip

A Note About the required attribute For native inputs, there is a required attribute that can be added to an input. When this attribute is present, the form will not be able to submit when a field is empty.

There's also aria-required="true" that indicates to Assistive Technology that a field is required. However, this does not prevent the form from being submitted without client-side JavaScript added.

Check out this article from the W3C for more info on required vs. aria-required.

Adding onChange Handlers to the Inputs

For each of the inputs in the form, we'll add an onChange event inline and bind it to a function called changeHandler that we will write in a moment.

Even though we have various types of inputs including text fields and a checkbox, we can reuse the same changeHandler function.

Above the return() of the component, create the changeHandler function that will take in the event object.

Inside of this function, we first need to get ahold of the target element from the event object. Then we'll check its value to see if it came from the checkbox or a text input.

We'll also grab the id attribute from the event.target.

Here's what the changeHandler looks like so far:

const changeHandler(event) => {
  const target = event.target
  const value = target.type === 'checkbox' ? : target.checked : target.value
  const id = target.id
}

Now we'll call setIsFormDirty and set it to true since we know the form was edited as the change event has been called on at least one of the controls.

Video: Adding onChange Handlers to the Inputs
Loaded: 2%
Current Time 0:00
/
Duration Time 3:31
Video Transcript

So we need to kind of keep track of all of our form inputs to do more granular individual level input, form validation.

So for native inputs, there is a required attribute. So we could use that the effect that will have. So if I hit refresh. That will say, please fill out this field. So that is an option, but we really want to test that native form validation in this in screen readers as well. So it will prevent us from submitting this form on the client side, at least.

And so that's the kind of functionality like I was talking about earlier, where we want to make sure we're testing it so we could add required. There's also things like ARIA dash required equals true. So the state are ARIA required state that won't prevent the field. You know, it won't use that native form validation, but it will indicate in assistive technology that the field is required.

So coming over here to the accessibility inspector, ARIA required we'll change this flag to true. So kind of the differences between the required boolean attribute and HTML and the aria attribute. So, yeah, we want to make sure we're testing required, like test the things that you add for sure.

But functionally, so if we want to show some custom error validation, let's add some on change handlers.

So I'm gonna say on change and we'll put a change handler change handler, and I'm going to put that on all of our inputs. I think they all can use it.

Yes. Even though we have various types. So we have number fields, we have text fields, the checkbox, and a text area. Those can all use a change handler. We need to come up here and define that. So I'm going to say const change handler. We'll pass in the event. Object, our handy friend love the event object.

So awesome.

So this is where, when we're typing, we can kind of change the state of the form, whether it's dirty or not. So if we wanted to create our own custom form validation, like let's say, let's say required, like we add it, but it's not quite working the way we expect in mobile voiceover or whatever screen reader you're trying to support.

You might want to come in and add more of your own functionality, which is what we're doing here. So in our change handler, I'm going to do a little bit of setup. So get ahold of the target. So the individual input that we were on let's check its value. So for this target, if it's type, depending on the type of, it might be slightly different things that we needed to grab.

So if it's a checkbox. We want to grab its target dot checked property. Otherwise it's target value if it's something the user typed in. So is what is that depending on what type of input it is or what type of control let's also grab a, an ID. I think these, yeah, these all have ids because their labels are matching.

Those IDs should be for the most part. So we've got that. So here's where we're going to call set is form dirty. We'll set that to true and we're going to set the form state. So that state that we had set up here with all of these values, this is where those default values come in is once those have changed, we can compare.

Updating State on Form Changes

Because each individual input can be updated, we need to compare the previous form state to its new value as the form onChange is called.

The spread operator (...) will help us set the new form state by merging its previous values with the updated value for an element with a given id.

This is a convenient way to essentially copy an object and overwrite only the part we need to.

setFormState(prevState => {
  return {...prevState, ...{
    [id]: value
  }}
})

All together, here's what the changeHandler function looks like:

const changeHandler(event) => {
  const target = event.target
  const value = target.type === 'checkbox' ? : target.checked : target.value
  const id = target.id

  setFormIsDirty(true)
  setFormState(prevState => {
    return {...prevState, ...{
      [id]: value
    }}
  })
}

Form Markup Updates

There are a few more things we can add to the form.

Conditional CSS Styling

We can add styling to the form depending on if isFormDirty is true or not, using a ternary operator and React className string:

<form
  action=""
  className={!isFormDirty ? `dirty` : ``}
  onSubmit={(event) => {submitHandler(event)}}
>

Add aria-invalid to Individual Fields

The aria-invalid attribute is a great way to inform a person using a screen reader that there's something wrong with a form field.

Starting with the first input for the submitter's name, we'll add aria-invalid and compute its value with an expression that checks if the form was submitted and if the field is empty.

If the expression is true, the attribute will be added with a true value. Otherwise, we'll set the attribute value to null so it won't be added:

// On the Name input
<input
  aria-invalid={
    isFormSubmitted &&
    formState.submittername.length === 0 ? 'true' : null
  }
  onChange={changeHandler}
  type="text"
  id="submittername"
>
📝Exercise

Exercise: Add aria-invalid checks for each of the remaining form fields and the checkbox.

Video: Form Updates
Loaded: 2%
Current Time 0:00
/
Duration Time 3:22
Video Transcript

So I'm going to compare the previous state. So inside of this object, we're going to compare its previous state to what's the state of everything. Now I'm a little bit crafty here, so I'm going to return an object and I'm using long waves. A way of saying that we're going to combine the previous state object with whatever we've typed in.

So what do you call that triple dots? That spread operator. Okay. So the spread operator So we've got a the previous state.

So this is that the way that we can kind of combine objects and not just like blow away what we had before we cut, we need that spread operator. Or at least it's really convenient to be able to combine objects with the spread operator. So we've got a change handler. Our set is formed dirty. We're going to make use of that on the form.

We can do a little bit of CSS styling on the form depending on whether it is dirty or not. So let's add a class name and we'll add an expression here. So if the form is not dirty or if is formed dirty,

For some reason I'm going to put, we'll see how this works. A string of dirty, this feels backwards to me, but we'll try it.

And then in the individual fields, we've got a couple of things we're going to do. We need to handle aria invalid. So ARIA invalid is a fantastic way to alert a screen reader, user that a field is there's something wrong with it.

And so we are going to add that to our inputs. So I'm going to come down here, organize these a little bit. So on our input, starting with the first one, I will say ARIA dash invalid. And inside of here, let's add an expression is the form submitted and check the check, whether its value is empty or not. So form state that submitter name dot length, if that the length of that is zero characters.

When you go to submit, then it, yeah. If it has empty characters, then it's not valid. Otherwise we will let that be null and not add that attribute. We could put false there as well. So for each one of those, I'm going to copy it in the interest of time. And we could probably just do a few of these for time's sake just to have a couple of them.

So rather than submitter name, this would be. Email, I believe. Yeah. Email we'll do the first three. How about that? Get some functionality working before we spend all this time doing all this wiring up, so ARIA invalid on the site name and we'll, we'll let that behave for now. So, so ARIA invalid, and then we've got one more thing with refs.

So we'll leave that for a moment. So in our submit handler, we've got a little bit more work to do up here. So we added the kind of overall global announcement. If we want to do a little more air handling for individual fields, we've got a change handler. That's doing stuff now, but then when we submit we want to do some more behavior.

Checking Progress in the Browser

Back in the browser, hit the submit button on the empty form.

Red outlines will show up for the form fields that are invalid thanks to the CSS that we already had defined for when aria-invalid is true.

Form fields where aria-invalid is true are given a red border using a CSS attribute selector.

Styling based on ARIA attributes is a useful technique that works great for forms.

There are still some design questions that can arise. For example, an invalid form field should be focusable. But when focus shifts to it, the border of the input is now blue instead of red. Should it stay red? For how long? Maybe that's not important, since if I focused on a field I'm probably about to type in it. Another subjective situation.

The important thing here is that we are able to not only visually represent the form's validity, but programmatically for Assistive Technology as well.

Video: Checking Form Styling in the Browser
Loaded: 2%
Current Time 0:00
/
Duration Time 3:29
Video Transcript

Let's see what's going on in our browser. Let's come in here. So if I start typing oh, cool. So I've got a style. Now, our ARIA invalid is actually working. So I've got, we added our invalid to the first couple of fields. And if we enter the form without those, we can actually use our aria attribute to affect the visual style.

So these red outlines, we can combine our ARIA attributes and our CSS. So in our CSS, we've already got wired up this attribute selector for aria invalid of true. And we can add a border. We could add a little icon with a CSS pseudo element or something. Using attributes aria attributes, in our CSS.

This is a great use case for that in your form validation. So that if you've got a specific time, when an ARIA state comes up, you can style based on off of that. You know, it's like a really solid use case that you can count on. Sometimes using aria, like you want to make sure it's not brittle, so that's why it's a little bit subjective about what might work the best, but for form validation and aria, invalid state, some other state use cases, that's where styling can be super handy.

So if we go through all of our fields and wire up or invalid, we've got a visual treatment there, as well as the screen reader information with the ARIA attributes

Well,

if it's invalid, it should definitely still be focusable. I mean, so we do kind of lose, like when I'm focused in here, I kind of lose the invalid outline because our focus outline is kind of stopping over that.

So maybe there is a use case for some external icon or, you know, something where. Your focus, isn't kind of, preventing you from seeing the invalid state, but yeah, that's kind of becomes a, a design question more than anything. And so yeah, we want to programmatically describe what's the state of the form as well as visually.

And so we're indexing much more heavily on the programmatic side of things right now, but we do have that issue where our focus state makes it. So you can't see that this is invalid, but maybe that's not a problem cause I'm in the field and I'm going to go type in it.

And so it's kind of, maybe it's not that important that it's invalid while I'm typing in the field. Sort of thinking through that a little bit more. Okay. So when we hit submit on any of these fields, like, let's say I put my name in here and then I go, you know, maybe I miss a field. Like I add my email or, you know, I think I get all the items in the form and I hit submit.

It would be cool if I could have my focus taken right to the field that I missed. So maybe I need to pluck that one field out of a list and send focus there. So we've got kind of screenreader announcements happening to make it really ergonomic to like, so I'm not having to go and hunt through the form of like, what field did I miss?

I mean, maybe that could go send focus to the first one that needs to be filled in. That'd be awesome. So let's do something like that where we can kind of keep track of what's been submitted and send focus to the first item that needs to be filled in. So we can do that in our submit handler.

🛠 Challenge: Send Focus to the First Invalid Field

At this point in development, when the user submits the form the live region will update with an error message but leave focus where it was.

It would be cool if after submitting, focus was sent directly to the first field with invalid input.

For this challenge, draw upon what you’ve learned about focus management and make it happen!

💡Tip

Hints: Start by creating a collection of inputRefs by initializing a useRef() React hook with an empty array and add a ref from each input dynamically in the markup. The focus logic you write will go in the submitHandler. Refer back to our work on the date picker component for more hints!

🛠 Solution: Sending Focus to the First Invalid Field

Step 1: Track Input Refs

The first thing we need to do is initialize our inputRefs at the top of the component near where we set up our state. Like with our date buttons before, we'll set it to useRef([]).

const inputRefs = useRef([]);

Now we need to add refs to the array dynamically for each of the text inputs.

Here's what the Name input looks like:

<input
  aria-invalid={
    isFormSubmitted && formState.submittername.length === 0 ? "true" : null
  }
  id="submittername"
  onChange={changeHandler}
  ref={(inputRef) => {
    inputRefs.current.push(inputRef);
  }}
  type="text"
/>

The process to add a ref will be the same for each of the inputs so feel free to do some copying and pasting.

It is worth pointing out that on further reflection I decided that neither the ownership checkbox nor the fee input should become invalid since a site might be free. Another example of how the use of the application can affect the coding decisions we make!

Step 2: Update the Submit Handler

Up inside of the submitHandler, we need to keep track of the elements in our form so we can grab the first empty one.

Create a new variable for firstEmptyElementIndex. We'll use the JavaScript let keyword so we can set the variable’s initial value to null and overwrite if we do find an empty element.

Then we will create an array of formElements by calling Array.from(event.target.elements).

Now we can iterate over the form elements, using a switch statement that looks at the elements’ type.

If it's the submit button, we'll return and break out of the switch.

For the default case for the switch statement, we will set isFormDirty to false since it's been submitted on the client-side. Then we'll add the logic that looks for empty form fields.

If we encounter an empty field and the firstEmptyElementIndex is null, we know we we've found our first empty element so we should set it to the current index. Then we can call inputRefs.current[index].focus() to send focus to it.

💡Tip

Remember, refs let us exit React land to use DOM methods on raw elements.

The call to setErrorAnnouncement will close out the default case of the switch statement.

Here's what the updated submitHandler looks like:

const submitHandler = (event) => {
  event.preventDefault();

  setIsFormSubmitted(true);

  let firstEmptyElementIndex = null;
  const formElements = Array.from(event.target.elements);
  formElements.map((element, index) => {
    switch (element.type) {
      // ignore the submit button
      case "submit":
        return;
        break;

      default:
        // set form state for aria-invalid
        setIsFormDirty(false);

        if (element.value.trim().length === 0) {
          // focus on first empty input when submitted
          if (firstEmptyElementIndex === null) {
            firstEmptyElementIndex = index;
            inputRefs.current[index].focus();
          }
          setErrorAnnouncement("Required fields cannot be empty.");
        }
        break;
    }
  });
};

Video: Solution: Sending Focus to the First Invalid Field
Loaded: 1%
Current Time 0:00
/
Duration Time 6:46
Video Transcript

We need to get a couple of refs for inputs. So let's do that set up here up at the top. Let's make a variable for our input refs we'll keep track of those with a use ref and similar to our date grid buttons. We're going to keep a collection of those.

So let's come down to our inputs and on each one of these, I will say ref, and then similar to our other our date functions, we're going to do something very similar. So input ref for each one of these refs we will add it to current by pushing it input ref and I can copy and paste to my heart's delight.

And you can too. So we'll add each one of these I can even do a little bit of craftiness and fill in some of these ARIA invalids while I'm in here. All right. Input dot push, and put ref, make sure I get the whole thing cause I missed part of it. Get it all. And this is the location field. Make sure I've got it all.

keep coming down here for our fee, the numerical one, the ref stuff is all the same. So I'm mostly just coming through here and copying and pasting ownership. Oh, that one is the check box. So that one could potentially be slightly different. The aria and valid, checkbox. I think I'm going to leave our invalid off of that one and we could also make it checked by default if we wanted input ref. Yeah. Kind of a question. Whether we want to send focus to this checkbox or not. We'll let that be. What that'd be good. So we've got quite a few of these change handlers on whether or not they've been filled in.

I might leave the fee. One leave the ARIA invalid off of that. Cause maybe it's free. Maybe it doesn't have a fee. Maybe it shouldn't be invalid. Thinking through that one. We'll leave the fee one. Getting too copy happy. Okay. So we've got some change some refs so that we can actually send focus to some of these items if they need attention.

So coming back up to our submit handler, let's get this closed out. So we need to keep track of some of the elements in in this submit handler. So up here after our set is formed submitted I'm going to grab the first empty element index. So if I have multiple fields that, you know, I say I miss two fields and I want to send focus to one of them.

I need to keep track of what the first one is like. I can't send focus to two elements at once. So I'm going to keep track of the first one that's empty. What, what index does it have? And so for that, I'll let this be initialized as null that index. And then I'm going to iterate over the form elements. So const form elements, I'm going to do a little crafty trick to grab an array of the event, target dot elements.

So this event was bound to the form. And so that is what event target is right here. Using the document dot forms API, I can go and grab its elements, which is pretty cool, or that might just be a Dom API. But because I have this event object. Even though we're in react land. This gives me access to the element children of the form that we've found the submit handler to.

So now let's iterate over these form elements. Our array, since I did array dot from, which is really nice for working with collections of elements, including for focus management, all kinds of use cases. So our first element in this array iterator and the index we can keep track of as well. So in here, we're going to do another switch statement and depending on the type of element, cause we have a few different types of inputs in here.

So for the submit button, we don't want it to do anything just return, get out of there and I'll do a break. Get out of that statement. So our next, actually our default case is going to be for all these form inputs. So set is form dirty. Finally, we're going to set that to false because we have submitted and yeah, we're not in that mixed state anymore.

We hit that submit button. So, you know, in terms of being like in the middle of a task that's over with now, so we're going to set that to false. So if the, so we're iterating over these, these inputs. So if our event element not, not event, if element dot value dot trim if that is equal to zero, so no characters.

I can do some stuff for focus. So if

first empty element index, if that equals null, cause that's our start case. I haven't set anything yet. First empty element index will equal the index of the item we're on. So we had to make this sort of dynamic, cause we don't know what, what field is empty. It could be one partway down the page. And so we need to keep track.

We're going to iterate over all of the inputs within the form and say, is it empty? Which, which one is empty. And so the first one that we encounter, we're going to store its index. That will be the element that we send focus to. And so our input refs come in handy here. So input refs dot current. So we've seen this before this approach using the input refs current, and then grabbing the specific index out of that.

That gives us access to an actual Dom node that we can send focus to. So one of the magical things about React refs and that current property is that you can call DOM methods on those elements, whereas otherwise you're in react land. So that's what we needed. So now in this case, in our default switch statement, now we're going to set that required fields cannot be empty and we need to break our switch.

So we don't get stuck in an infinite loop. So we've got this iterator going over our form inputs. We've got a default case so that it doesn't get tripped up over the submit button. We can get this case for the rest of the inputs. Kind of clear out that form dirtiness cause we've submitted. And if any, if there is an empty field, we will send focus.

And the required fields can not be empty. Let's hit save.

Checking Focus Management & VoiceOver

Back in the browser, we can can confirm that our focus is jumped to the first invalid field after we try to submit the form, just like we expected!

When we test the form with VoiceOver in Safari, we also get the behavior we expect.

However, from the announcement there's another opportunity for making the experience better.

The "invalid data" message could be customized based on the form field it came from. Or going further, what if the error was a network error? The data might be valid but the form couldn't submit. Something else to think about.

📝Exercise

Exercise: As an exercise on your own, update the error messages to be more specific.

Video: Checking Focus Management & VoiceOver
Loaded: 2%
Current Time 0:00
/
Duration Time 3:58
Video Transcript

So in here, if I hit enter our global error message still works, it will still announce in the screen reader. And now we have these individual fields. So like if I type my name so that invalid state went away, it's our form is still kind of in its mixed, dirty state.

But when I type in a field it's, it's satisfying its case. It doesn't have an empty value. If I hit enter, like, let's say I'm further down the form. And you know, I thought I got all the fields, but I didn't and I hit enter. Now it sends me back to that first empty field. So that submit handler and I'm like, okay marcy@testingaccessibility.com.

Okay. I got my email address now and the site name. This is really funny one, it's actually a real place. Totes Cooley totes. So maybe I'm going to submit Totes Cooley cause it's got really amazing camping and it should be on the site. But I thought I got all the fields and I didn't. So here's where the native form validation is coming in for our our email address for some reason that wasn't working.

So I probably need to go and delete that pattern. It was something, you know, I've really played with that regular expression on that input pattern and it still doesn't work cause it's complaining about. So you might have some issues with native form validation. But we did see our, our submit. So if my focus is somewhere else, it will send my focus back to the place I need to be.

So let's fire this up in a screen reader, cause that's ultimately what we're playing with here. Right? So we'll fire up the screen reader and hear what it says when something's invalid.

So let's fire this up with safari and voiceover and we'll check NVDA too. Okay. So I'm going to fire up voiceover using function, command F five voiceover on safari camp spots window submit your spot group has keyboard focus.

You are currently on your name, star edit text article. Do you open the required fields can not be empty bloop. Okay. I'm going to type a name and we'll see what happens with our ARIA invalid your email address, star inlet data, edit text menu, your name star invalid data, edit text menu. So invalid data you know, because of a network problem or something may or might not work.

Maybe like. Custom error message might be more appropriate. And you know, that could include instructions to refresh the page or something, or maybe a network outage message or something. Cause this very specifically says invalid data. It is tied to the fact that we're in a form input. So like the type of element that you put it on matters as well and there's requirements.

And the aria specification does list kind of what's allowed where, and what's required, all that kind of thing. So if I fill in this field and then we'll test the focus ability piece to O C Y Marcy, do you open the autofill menu, press the up or down arrow key. Then site name, star invalid data. Edit text menu, your

email address, star invalid data, edit text.

Cool. I don't need to open autofill, get it, stop reading. But we've got kind of a combination here of live region announcement using ARIA valid and focus management to get us to the last field that the user missed in a really long field. All of those pieces can make a form more informative and, you know, we've got lots of tricks that we can employ for various form scenarios, but it's nice to have multiple tools.

One of the tools at our disposal is using these asynchronous updates, you know, these status messages and things. So we have a role alert. One thing we could play with is changing it from role alert to role status. So all of these fields have a, an asterisk.

There is also kind of a key down here that says fields are required. So it explains visually what the asterisk is for. But are you required would be nice to add, so let's go add that .

Adding aria-required to Required Form Fields

To experiment with ARIA vs. native HTML form validation, we are going to add the aria-required attribute and set it to true for each of the form fields that have been visually marked as required:

<input
  aria-invalid={...}
  aria-required="true"
  ...

Saving our work and jumping back to the browser, we’ll start VoiceOver again.

VoiceOver does announce that a field is required but it also says the word “star” along with the label.

Announcement Considerations

Here’s the markup for the location label. Note that htmlFor in React/JSX renders as for in the browser to pair a label with an input.

<input
  aria-invalid={...}
  aria-required="true"
  ...

It doesn't seem that the screen reader does anything with the abbr attribute for an accessible name, because it isn't a valid attribute in this context. (abbr can only be used on table headers, in fact.)

We could tell it to ignore the asterisk by setting aria-hidden="true" on the span, which would be fine since a screen reader user would still be informed that the field was required. But also, I subjectively wonder if "star" could be considered helpful content. It’s not the same situation as VoiceOver reading out a decorative “right-angled bracket” or “accent circumflex”, for example. Another option would include trying the <abbr> element with a title attribute explaining what the asterisk stands for.

This is another situation where your team could decide what to do based on feedback from daily screen reader users.

The important part is that you aren’t relying only on the user having to know that “star” means “required” when the Screen Reader announces it.

Video: Adding aria-required to Required Form Fields
Loaded: 2%
Current Time 0:00
/
Duration Time 3:48
Video Transcript

Come over here to vs code. Let's see kind of how these are, are marked up. So if, if they're all required, so we've got kind of a, an abbreviation here for require that may or may not be announced.

I think programmatically saying are required would be great way to go. ARIA Dash required. We'll say. True. And so if all fields are required, then stands to reason we can put this on. All of them are ARIA required. All of them. Let's put them all on here.

Make sure I get them all.

Oh, it says fields are required. So not all required. They're not all required. The ones with asterisks. So nightly fee is not because it could be free. Right? So that one doesn't need to be. But any of the ones that do have this asterisk. So here's another testing note.

So there's an abbr for an abbreviation on this asterisk. It's it wasn't really getting read aloud. So let's come back over here. I might leave one of these ARIA requires off so that we can compare, like, did that abbreviation, this is part of the label. So it should be exposed. As part of like it's related to the input, they've been bound together.

And so, are ARIA required, you know, if, if it was part of the label, it would be spoken in there. But are ARIA required more of a programmatic way to mark something as required, which is really powerful and super worth it.

So coming back over here to safari, I'm gonna hit refresh again and let's fire up voiceover.

Voiceover on safari camp spots, window camp spots, welcome link, skip to main content banner. You are your name star required, edit text with autofill menu, your name star. So yeah, the abbr is not actually doing anything. This is kind of similar to things we ran into with our date picker, with voiceover, where you might go and add things to like table headers.

And, you know, the structure may or may not be read aloud, even though you're using attributes or sometimes entire elements that should work, that should have semantic meaning, but they don't. And so when we test it in the screen reader, we might find it needs more information. So yes, we could suppress the asterisk from assistive technology with an aria hidden of true since star.

I dunno, it is a little bit subjective, but cause then if we hide that, then does it make sense to have this star fields required? Maybe, maybe not. I don't think that's a huge deal. As long as we have something that does say required. So it's not re relying on this cognitive understanding of what asterisk means.

It's additional information in this case. It's not some like, you know, it's not a custom angle bracket. That's like a stylistic arrow or a bullet in a list or something like the asterisk. I dunno, this is so subjective territory having the programmatic required bit is really nice. Like that part is helpful, whether, I mean, I would recommend ARIA required or the required attribute.

Either one will communicate this piece whether or not we hide the asterisk. It's, it's kind of an open question. But if I hit enter your required fields, We've got our same functionality. I could type my name in oh, Y Marcy closing menu, Marcy insertion at end of text, your name star required. Edit text.

Your email address star required into the data. Edit text menu required invalid data. So that gives us some really great information programmatically. We've got the focus management, we've got a live region announcement going on. There's a lot of good stuff going on here. So I'm going to close that voiceover voiceover off.

Checking Our Work in NVDA on Windows

We’ve seen how VoiceOver reads our page. Now we’ll check it with NVDA on Windows.

Before we do, let’s replace the live region’s role from alert to status to compare the behavior:

<p aria-relevant="all" className="error" role="status">

The status role will wait for other screen reader messages to complete before making an announcement, rather than interrupting and squashing other output. Check out the video below to listen to how NVDA makes announcements.

Section Wrap-Up

As you move forward with announcements, remember that the element with a live region role or aria-live attribute has to be rendered to the page at load time. Then, text content can be added to it on demand and read out.

If you try to append the actual live region itself just-in-time for the announcement, it’s probably too late.

We did a bunch of stuff in this section. Use the code here in your own experiments with ARIA attributes, form validation, custom error messages, or anything else to further your understanding and skills. And remember to test these techniques with Assistive Technology and the keyboard!

Video: Checking work in NVDA & Section Wrap-Up
Loaded: 2%
Current Time 0:00
/
Duration Time 3:57
Video Transcript

Let's go test this in, NVDA on windows

and to give us something unique. Number lock on, let's change our something unique to play with in NVDA let's change this from role alert to role status, and see if that has any difference. So I'm going to come back over here to Chrome on windows. Lock on. Number lock on. Really wants to tell us better and resources our insert key.

So let's go over to our submit list. Banner land resources region list with one item. Submit your spot link clickable banner landmark list with one item. Skip out of list. Main landmark region heading level two. Submit your spot heading level three godick main landmark. Submit your spot region document your name star edit required blank.

M R Y. Your email address star editor required. Fields can not be. Cool. So our announcement worked, I think because we didn't have any other site named star edit required, invalid entry, blank, our our announcement worked because we didn't have any other announcements happening at the same time. It sort of didn't really make a difference between assertive or polite or alert or status.

If we were doing other things and there was announcements kind of happening asynchronously, like some message just popped into the page and we were doing other things that is where those politeness levels can really make a difference. But in this case, it's kind of, we're like interacting with the form.

So it didn't really change much. But it announced it all the same. So we had some asynchronous stuff happening. You know, my focus is still in this form field and I can have. Make that announcement without having to send my focus directly to that item. So live regions are really powerful. We use them in all kinds of scenarios, as I noted, they need to be rendered, so they need to be present when the page loads.

And then we append text to them as opposed to try and do a just in time announcement in, you know, appending the live region when you're making the announcement. It's probably too late. Like if it works, it might be a fluke. It definitely wouldn't be a reliable cross cross-platform so making them available on the page loads and yeah, try not to, you know, obliterate the experience with something like a marquee that's raining all the time.

You could maybe make the worst website ever if you want, like the worst accessible website ever that like, if we could get to that point, that could be like entertainment for fun. Not, not on a real site. But live regions are awesome. So form validation is one use case toasts chat, widgets client side routing, live regions are super helpful for all kinds of scenarios.

We saw a bunch of stuff in this example, not just live regions a bunch on form validation and yeah. ARIA invalid. So playing with different aria attributes you know, it's going to take some experimentation and definitely some testing, like you might try using something like native form validation, like this email pattern, I'm just going to delete that it didn't work well.

Like when I had an email address in there that regex pattern, I mean, we could even look at it. It's, you know, I, I looked on the internet like you do to find a an input pattern like that. It'd be better, probably better to, instead of using the native input validation, maybe write your own function or.

I have a colleague who's really good at regex help you, not my strong suit. It's okay. We can't all be good at everything. Regex can be fun, but not always. Okay. So announcements with assistive technology. We've seen them in action. We've played with them. You can use them for all kinds of scenarios.